﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using FFSupplier.ServiceCore.Interface.CrmConnection;
using FFSupplier.ServiceCore.Model;
using Microsoft.Xrm.Sdk;

using Microsoft.Xrm.Sdk.Query;
using Microsoft.Xrm.Tooling.Connector;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel.Description;
using System.ServiceModel;
using Microsoft.Xrm.Sdk.Client; 

namespace FFSupplier.ServiceCore.Impl.CrmConnection
{
    public class CrmOrganizationServiceProxy : ICrmOrganizationServiceProxy
    {
        private static IServiceManagement<IOrganizationService> orgServiceManagement = null;
        private static AuthenticationCredentials crmAuthenticationCredentials = null;
        private OrganizationServiceProxy _serviceProxy;

        public CrmOrganizationServiceProxy(CrmOrgaizationConfiguration configServer)
        {
            ServicePointManager.ServerCertificateValidationCallback = delegate (object s, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { return true; };

            IServiceManagement<IOrganizationService> orgServiceManagement = InitServiceManagement(configServer);
            OrganizationServiceProxy proxy = null;
            if (crmAuthenticationCredentials.SecurityTokenResponse != null)
            {
                proxy = new OrganizationServiceProxy(orgServiceManagement, crmAuthenticationCredentials.SecurityTokenResponse);
            }
            else
            {
                proxy = new ManagedTokenOrganizationServiceProxy(orgServiceManagement, crmAuthenticationCredentials.ClientCredentials);
                proxy.Authenticate();
            }

            _serviceProxy = proxy;
            try
            {
                int timeout = configServer.TokenSeconds > 0 ? configServer.TokenSeconds : 20;
                _serviceProxy.Timeout = TimeSpan.FromSeconds(timeout);
                _serviceProxy.ServiceChannel.Open();
                _serviceProxy.EnableProxyTypes();
                _serviceProxy.CallerId = configServer.CrmAdministratorId;
            }
            catch
            {
            }
        }

        private static IServiceManagement<IOrganizationService> InitServiceManagement(CrmOrgaizationConfiguration configServer)
        {
            // 链接生产环境后，需要手工判断Token超时并重新链接
            if (orgServiceManagement == null || crmAuthenticationCredentials == null ||
                crmAuthenticationCredentials.SecurityTokenResponse == null ||
                crmAuthenticationCredentials.SecurityTokenResponse.Token == null ||
                DateTime.UtcNow.AddMinutes(5) > crmAuthenticationCredentials.SecurityTokenResponse.Token.ValidTo)
            {
                string serverUrl = configServer.ServerUrl;
                orgServiceManagement = ServiceConfigurationFactory.CreateManagement<IOrganizationService>(new Uri(serverUrl));
                AuthenticationCredentials cred = new AuthenticationCredentials();
                if (orgServiceManagement.AuthenticationType == AuthenticationProviderType.ActiveDirectory)
                {
                    cred.ClientCredentials.Windows.ClientCredential.Domain = configServer.ServerUserDomin;
                    cred.ClientCredentials.Windows.ClientCredential.UserName = configServer.ServerUserName;
                    cred.ClientCredentials.Windows.ClientCredential.Password = configServer.ServerUserPassword;
                }
                else/* if (orgServiceManagement.AuthenticationType == AuthenticationProviderType.Federation)*/
                {
                    cred.ClientCredentials.UserName.UserName = string.Format("{0}@{1}.local", configServer.ServerUserName, configServer.ServerUserDomin);
                    cred.ClientCredentials.UserName.Password = configServer.ServerUserPassword;
                }
                crmAuthenticationCredentials = orgServiceManagement.Authenticate(cred);
            }
            return orgServiceManagement;
        }

        public void Associate(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities)
        {
            _serviceProxy.Associate(entityName, entityId, relationship, relatedEntities);
        }

        public Guid Create(Entity entity)
        {
            return _serviceProxy.Create(entity);
        }

        public void Delete(string entityName, Guid id)
        {
            _serviceProxy.Delete(entityName, id);
        }

        public void Disassociate(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities)
        {
            _serviceProxy.Disassociate(entityName, entityId, relationship, relatedEntities);
        }

        public OrganizationResponse Execute(OrganizationRequest request)
        {
            return _serviceProxy.Execute(request);
        }

        public Entity Retrieve(string entityName, Guid id, ColumnSet columnSet)
        {
            return _serviceProxy.Retrieve(entityName, id, columnSet);
        }

        public EntityCollection RetrieveMultiple(QueryBase query)
        {
            return _serviceProxy.RetrieveMultiple(query);
        }

        public void Update(Entity entity)
        {
            _serviceProxy.Update(entity);
        }

        public void Dispose()
        {
            _serviceProxy.ServiceChannel.Close();
            _serviceProxy.Dispose();
        }
    }
    /// <summary>
    /// Wrapper class for OrganizationServiceProxy to support auto refresh security token
    /// </summary>
    internal sealed class ManagedTokenOrganizationServiceProxy : OrganizationServiceProxy
    {
        private AutoRefreshSecurityToken<OrganizationServiceProxy, IOrganizationService> _proxyManager;

        public ManagedTokenOrganizationServiceProxy(Uri serviceUri, ClientCredentials userCredentials)
            : base(serviceUri, null, userCredentials, null)
        {
            this._proxyManager = new AutoRefreshSecurityToken<OrganizationServiceProxy, IOrganizationService>(this);
        }

        public ManagedTokenOrganizationServiceProxy(IServiceManagement<IOrganizationService> serviceManagement,
            SecurityTokenResponse securityTokenRes)
            : base(serviceManagement, securityTokenRes)
        {
            this._proxyManager = new AutoRefreshSecurityToken<OrganizationServiceProxy, IOrganizationService>(this);
        }

        public ManagedTokenOrganizationServiceProxy(IServiceManagement<IOrganizationService> serviceManagement,
            ClientCredentials userCredentials)
            : base(serviceManagement, userCredentials)
        {
            this._proxyManager = new AutoRefreshSecurityToken<OrganizationServiceProxy, IOrganizationService>(this);
        }

        protected override SecurityTokenResponse AuthenticateDeviceCore()
        {
            return this._proxyManager.AuthenticateDevice();
        }

        protected override void AuthenticateCore()
        {
            this._proxyManager.PrepareCredentials();
            base.AuthenticateCore();
        }

        protected override void ValidateAuthentication()
        {
            this._proxyManager.RenewTokenIfRequired();
            base.ValidateAuthentication();
        }
    }

    /// <summary>
    /// Class that wraps acquiring the security token for a service
    /// </summary>
    public sealed class AutoRefreshSecurityToken<TProxy, TService>
        where TProxy : ServiceProxy<TService>
        where TService : class
    {
        private ClientCredentials _deviceCredentials;
        private TProxy _proxy;

        /// <summary>
        /// Instantiates an instance of the proxy class
        /// </summary>
        /// <param name="proxy">Proxy that will be used to authenticate the user</param>
        public AutoRefreshSecurityToken(TProxy proxy)
        {
            if (null == proxy)
            {
                throw new ArgumentNullException("proxy");
            }

            this._proxy = proxy;
        }

        /// <summary>
        /// Prepares authentication before authen6ticated
        /// </summary>
        public void PrepareCredentials()
        {
            if (null == this._proxy.ClientCredentials)
            {
                return;
            }

            switch (this._proxy.ServiceConfiguration.AuthenticationType)
            {
                case AuthenticationProviderType.ActiveDirectory:
                    this._proxy.ClientCredentials.UserName.UserName = null;
                    this._proxy.ClientCredentials.UserName.Password = null;
                    break;
                case AuthenticationProviderType.Federation:
                case AuthenticationProviderType.LiveId:
                    this._proxy.ClientCredentials.Windows.ClientCredential = null;
                    break;
                default:
                    return;
            }
        }

        /// <summary>
        /// Authenticates the device token
        /// </summary>
        /// <returns>Generated SecurityTokenResponse for the device</returns>
        public SecurityTokenResponse AuthenticateDevice()
        {
            if (null == this._deviceCredentials)
            {
                this._deviceCredentials = DeviceIdManager.LoadOrRegisterDevice(
                    this._proxy.ServiceConfiguration.CurrentIssuer.IssuerAddress.Uri);
            }

            return this._proxy.ServiceConfiguration.AuthenticateDevice(this._deviceCredentials);
        }

        /// <summary>
        /// Renews the token (if it is near expiration or has expired)
        /// </summary>
        public void RenewTokenIfRequired()
        {
            if (null != this._proxy.SecurityTokenResponse &&
                DateTime.UtcNow.AddMinutes(15) >= this._proxy.SecurityTokenResponse.Response.Lifetime.Expires)
            {
                try
                {
                    this._proxy.Authenticate();
                }
                catch (CommunicationException)
                {
                    if (null == this._proxy.SecurityTokenResponse ||
                        DateTime.UtcNow >= this._proxy.SecurityTokenResponse.Response.Lifetime.Expires)
                    {
                        throw;
                    }

                    // Ignore the exception 
                }
            }
        }
    }
}
